import MapboxMaps
import MapboxNavigationCore
import UIKit

/// The ``CarPlayMapViewControllerDelegate`` protocol provides methods for reacting to events during free-drive
/// navigation or route previewing in ``CarPlayMapViewController``.
public protocol CarPlayMapViewControllerDelegate: AnyObject, UnimplementedLogging {
    /// Tells the receiver that the final destination `PointAnnotation` was added to the ``CarPlayMapViewController``.
    /// - Parameters:
    ///   - carPlayMapViewController: The ``CarPlayMapViewController`` object.
    ///   - finalDestinationAnnotation: The point annotation that was added to the map view.
    ///   - pointAnnotationManager: The object that manages the point annotation in the map view.
    @available(
        *,
        deprecated,
        message: "This method is deprecated and should no longer be used, as the final destination annotation is no longer added to the map. Use the corresponding delegate methods to customize waypoints appearance."
    )
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        didAdd finalDestinationAnnotation: PointAnnotation,
        pointAnnotationManager: PointAnnotationManager
    )

    /// Asks the receiver to return a `LineLayer` for the route line, given a layer identifier and a source identifier.
    /// This method is invoked when the map view loads and any time routes are added.
    /// - Parameters:
    ///   - carPlayMapViewController: The ``CarPlayMapViewController`` object.
    ///   - identifier: The `LineLayer` identifier.
    ///   - sourceIdentifier: Identifier of the source, which contains the route data that this method would style.
    /// - Returns: A `LineLayer` that is applied to the route line.
    /// - SeeAlso: ``CarPlayManagerDelegate/carPlayManager(_:routeLineLayerWithIdentifier:sourceIdentifier:for:)``.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        routeLineLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> LineLayer?

    /// Asks the receiver to return a `LineLayer` for the casing layer that surrounds route line, given a layer
    /// identifier and a source identifier.
    /// This method is invoked when the map view loads and any time routes are added.
    /// This method is invoked when the map view loads and any time routes are added.
    /// - Parameters:
    ///   - carPlayMapViewController: The ``CarPlayMapViewController`` object.
    ///   - identifier: The `LineLayer` identifier.
    ///   - sourceIdentifier: Identifier of the source, which contains the route data that this method would style.
    /// - Returns: A `LineLayer` that is applied as a casing around the route line.
    /// - SeeAlso:
    /// ``CarPlayManagerDelegate/carPlayManager(_:routeCasingLineLayerWithIdentifier:sourceIdentifier:for:)``.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        routeCasingLineLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> LineLayer?

    /// Asks the receiver to return a `LineLayer` for highlighting restricted areas portions of the route, given a layer
    /// identifier and a source identifier.
    ///  This method is invoked when the map view loads and any time routes are added.
    /// - Parameters:
    ///   - carPlayMapViewController: The ``CarPlayMapViewController`` object.
    ///   - identifier: The `LineLayer` identifier.
    ///   - sourceIdentifier: Identifier of the source, which contains the route data that this method would style.
    /// - Returns: `LineLayer` that is applied as restricted areas on the route line.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        routeRestrictedAreasLineLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> LineLayer?

    /// Asks the receiver to adjust the default layer which will be added to the map view and return a `Layer`.
    ///  This method is invoked when the map view loads and any time a layer will be added.
    /// - Parameters:
    ///   - carPlayMapViewController: The ``CarPlayMapViewController`` object.
    ///   - layer: A default `Layer` generated by the carPlayMapViewController.
    /// - Returns: An adjusted `Layer` that will be added to the map view by the SDK.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        willAdd layer: Layer
    ) -> Layer?

    // MARK: Customizing Waypoint(s) Appearance

    /// Asks the receiver to return a `CircleLayer` for waypoints, given an identifier and source.
    /// The returned layer is added to the map below the layer returned by
    /// ``CarPlayMapViewController/navigationMapView(_:waypointSymbolLayerWithIdentifier:sourceIdentifier:)``.
    /// This method is invoked any time waypoints are added or shown.
    /// - Parameters:
    ///   - carPlayMapViewController: The `CarPlayMapViewController` object.
    ///   - identifier: The `CircleLayer` identifier.
    ///   - sourceIdentifier: Identifier of the source, which contains the waypoint data that this method would style.
    /// - Returns: A `CircleLayer` that the map applies to all waypoints.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        waypointCircleLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> CircleLayer?

    /// Asks the receiver to return a `SymbolLayer` for waypoint symbols, given an identifier and source.
    /// The returned layer is added to the map above the layer returned by
    /// ``CarPlayMapViewController/navigationMapView(_:waypointCircleLayerWithIdentifier:sourceIdentifier:)``.
    /// This method is invoked any time waypoints are added or shown.
    /// - Parameters:
    ///   - carPlayMapViewController: The `CarPlayMapViewController` object.
    ///   - identifier: The `SymbolLayer` identifier.
    ///   - sourceIdentifier: Identifier of the source, which contains the waypoint data that this method would style.
    /// - Returns: A `SymbolLayer` that the map applies to all waypoint symbols.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        waypointSymbolLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> SymbolLayer?

    /// Asks the receiver to return a `FeatureCollection` that describes the geometry of waypoints.
    ///
    /// For example, to customize the appearance of intermediate waypoints by adding an image follow these steps:
    ///
    /// 1. Implement the ``CarPlayMapViewControllerDelegate/carPlayMapViewController(_:shapeFor:legIndex:)``
    /// method to provide a
    /// `FeatureCollection` for waypoints.
    /// Within this method:
    ///     1. Add an image to the map by calling `MapboxMap.addImage(_:id:stretchX:stretchY:)` method.
    ///     2. Iterate through the `waypoints` array and create `Feature` for each waypoint.
    ///     3. Add a key-value pair to `Feature.properties` for specifying an icon image if the waypoint is
    ///     intermediate.
    ///
    /// Example:
    ///
    /// ```swift
    /// func carPlayMapViewController(
    ///     _ carPlayMapViewController: CarPlayMapViewController,
    ///     shapeFor waypoints: [Waypoint],
    ///     legIndex: Int
    /// ) -> FeatureCollection? {
    ///     let navigationMapView = carPlayMapViewController.navigationMapView
    ///     let imageId = "intermediateWaypointImageId"
    ///     if !navigationMapView.mapView.mapboxMap.imageExists(withId: imageId) {
    ///         do {
    ///             try navigationMapView.mapView.mapboxMap.addImage(
    ///                 UIImage(named: "waypoint")!,
    ///                 id: imageId,
    ///                 stretchX: [],
    ///                 stretchY: []
    ///             )
    ///         } catch {
    ///             // Handle the error
    ///             return nil
    ///         }
    ///     }
    ///     return FeatureCollection(
    ///         features: waypoints.enumerated().map { waypointIndex, waypoint in
    ///             var feature = Feature(geometry: .point(Point(waypoint.coordinate)))
    ///             var properties: [String: JSONValue] = [:]
    ///             properties["waypointCompleted"] = .boolean(waypointIndex <= legIndex)
    ///             properties["waypointIconImage"] = waypointIndex > 0 && waypointIndex < waypoints.count - 1
    ///             ? .string(imageId)
    ///             : nil
    ///             feature.properties = properties
    ///             return feature
    ///         }
    ///     )
    /// }
    /// ```
    ///
    /// 2. Implement the
    /// ``CarPlayMapViewControllerDelegate/carPlayMapViewController(_:waypointSymbolLayerWithIdentifier:sourceIdentifier:)``
    /// method to provide a custom `SymbolLayer`.
    ///     1. Create a `SymbolLayer`.
    ///     2. Set `SymbolLayer.iconImage` to an expression `Exp` to retrieve the icon image name based on the
    ///     properties defined in step 1.3.
    ///
    /// Example:
    /// ```swift
    /// func carPlayMapViewController(
    ///     _ carPlayMapViewController: CarPlayMapViewController,
    ///     waypointSymbolLayerWithIdentifier identifier: String,
    ///     sourceIdentifier: String
    /// ) -> SymbolLayer? {
    ///
    ///     var symbolLayer = SymbolLayer(id: identifier, source: sourceIdentifier)
    ///     let opacity = Exp(.switchCase) {
    ///         Exp(.any) {
    ///             Exp(.get) {
    ///                 "waypointCompleted"
    ///             }
    ///         }
    ///         0
    ///         1
    ///     }
    ///     symbolLayer.iconOpacity = .expression(opacity)
    ///     symbolLayer.iconImage = .expression(Exp(.get) { "waypointIconImage" })
    ///     symbolLayer.iconAnchor = .constant(.bottom)
    ///     symbolLayer.iconOffset = .constant([0, 15])
    ///     symbolLayer.iconAllowOverlap = .constant(true)
    ///     return symbolLayer
    /// }
    /// ```
    ///
    /// - Parameters:
    ///   - carPlayMapViewController: The ``CarPlayMapViewController`` object.
    ///   - waypoints: The waypoints to be displayed on the map.
    ///   - legIndex: The index of the current leg during navigation.
    /// - Returns: Optionally, a `FeatureCollection` that defines the shape of the waypoint, or `nil` to use the default
    /// behavior.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        shapeFor waypoints: [Waypoint],
        legIndex: Int
    ) -> FeatureCollection?

    @_spi(MapboxInternal)
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        didSetup navigationMapView: NavigationMapView
    )
}

extension CarPlayMapViewControllerDelegate {
    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        didAdd finalDestinationAnnotation: PointAnnotation,
        pointAnnotationManager: PointAnnotationManager
    ) {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayNavigationViewController,
        routeLineLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> LineLayer? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayNavigationViewController,
        routeCasingLineLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> LineLayer? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayNavigationViewController,
        routeRestrictedAreasLineLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> LineLayer? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        willAdd layer: Layer
    ) -> Layer? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        shapeFor waypoints: [Waypoint],
        legIndex: Int
    ) -> FeatureCollection? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        waypointCircleLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> CircleLayer? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    /// `UnimplementedLogging` prints a warning to standard output the first time this method is called.
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        waypointSymbolLayerWithIdentifier identifier: String,
        sourceIdentifier: String
    ) -> SymbolLayer? {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
        return nil
    }

    @_spi(MapboxInternal)
    public func carPlayMapViewController(
        _ carPlayMapViewController: CarPlayMapViewController,
        didSetup navigationMapView: NavigationMapView
    ) {
        logUnimplemented(protocolType: CarPlayMapViewControllerDelegate.self, level: .info)
    }
}
